Skip to content

feat: implement Merchant model (Closes #39) - #45

Merged
codebestia merged 6 commits into
ShadeProtocol:mainfrom
giftexceed:feat/39-merchant-model
Jul 24, 2026
Merged

feat: implement Merchant model (Closes #39)#45
codebestia merged 6 commits into
ShadeProtocol:mainfrom
giftexceed:feat/39-merchant-model

Conversation

@giftexceed

@giftexceed giftexceed commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the Merchant model, mirroring the Shade backend's Prisma schema with camelCase (JSON) → snake_case (Python) field mapping. merchant_id (from Prisma merchantId: Int) is the numeric identifier the Soroban contract stamps onto every invoice — the bridge between the backend and the on-chain world.

Closes #39

Design note

The issue text suggested Pydantic, but Pydantic is not a dependency of this SDK (only httpx + stellar-sdk) and the codebase is deliberately plain-Python. This PR implements the model in plain Python — matching the existing style, adding no new dependency, and reusing stellar-sdk (already present) for Stellar key validation. All acceptance criteria are met.

Changes

  • ShadeObject (src/shade/base.py) — base class for API resources: from_dict/to_dict with a per-class camelCase↔snake_case alias table, ignores unknown keys (additive backend changes stay safe), plus equality and repr.
  • Merchant (src/shade/merchant.py):
    • Explicitly typed fields — no generic settings dict.
    • merchant_id coerced to int (rejects bools/non-numerics).
    • address validated as a Stellar ed25519 public key (starts with G, 56 chars) → raises InvalidRequestError on construction.
    • display_name computed property: business_name"{first_name} {last_name}".strip()email.
  • Exported Merchant and ShadeObject from the package.

Acceptance criteria

  • Merchant.from_dict(api_response) maps camelCase → snake_case attributes
  • merchant.merchant_id is an int
  • A non-Stellar address raises InvalidRequestError on construction
  • merchant.display_name returns the most informative available name
  • No generic settings dict — all fields explicitly typed

Testing

tests/test_merchant.py — 14 tests covering mapping, unknown-key tolerance, merchant_id typing, address validation (bad key + wrong length), display_name fallbacks, optional defaults, and camelCase round-tripping.

146 passed

Summary by CodeRabbit

  • New Features
    • Added a public Merchant model with validated fields and display_name resolution/precedence.
    • Introduced a shared base object that converts between API camelCase payloads and Python snake_case objects, including to_dict/from_dict round-trip serialization and stable equality.
  • Bug Fixes
    • Strengthened validation for merchant_id, Stellar ed25519 address, and boolean flags (active/verified), with request errors indicating the failing parameter.
    • Safely ignores unknown fields when loading merchant data.
  • Tests
    • Added coverage for mapping, validation, display_name trimming/precedence, export behavior, and serialization round-trips.

Add ShadeObject base class and Merchant model with camelCase (JSON) <->
snake_case (Python) mapping via a per-class alias table and a from_dict
constructor.

- ShadeObject: from_dict/to_dict alias mapping, ignores unknown keys,
  equality and repr driven by constructor params.
- Merchant: explicitly typed fields (no generic settings dict); merchant_id
  coerced to int; address validated as a Stellar ed25519 public key
  (raises InvalidRequestError on construction); display_name computed
  property (business_name -> full name -> email).
- Export Merchant and ShadeObject from the package.
- Tests covering mapping, validation, display_name and round-tripping.

Closes ShadeProtocol#39
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@giftexceed, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5843d951-02ed-46e8-a4fa-56e9046a3078

📥 Commits

Reviewing files that changed from the base of the PR and between 27304be and 9530f3b.

📒 Files selected for processing (2)
  • src/shade/models/merchant.py
  • tests/test_merchant.py
📝 Walkthrough

Walkthrough

Adds ShadeObject for typed API resource serialization and validated Merchant models with camelCase mapping, Stellar address checks, strict field validation, display-name fallbacks, package exports, and comprehensive tests.

Changes

Merchant model

Layer / File(s) Summary
Resource serialization base
src/shade/base.py
ShadeObject maps camelCase payloads to constructor arguments, serializes instances back to dictionaries, and implements constructor-based equality and representation.
Merchant model and validation
src/shade/models/merchant.py, src/shade/merchant.py
Merchant models define backend fields and aliases, validate Stellar addresses and typed identifiers/flags, and compute display_name fallbacks.
Public exports and behavior tests
src/shade/__init__.py, src/shade/models/__init__.py, tests/test_merchant.py
The package exports Merchant and ShadeObject; tests cover mapping, validation errors, defaults, display names, unknown fields, and round-trip serialization.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant API
  participant ShadeObject
  participant Merchant
  participant StellarStrKey
  API->>ShadeObject: from_dict(camelCase payload)
  ShadeObject->>Merchant: construct filtered snake_case fields
  Merchant->>StellarStrKey: validate address
  StellarStrKey-->>Merchant: validation result
  Merchant-->>ShadeObject: validated Merchant
  ShadeObject-->>API: to_dict() camelCase payload
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly summarizes the main change: implementing the Merchant model.
Description check ✅ Passed It includes the summary, linked issue, motivation, implementation details, acceptance criteria, and testing, though it doesn't strictly mirror the template's checkbox sections.
Linked Issues check ✅ Passed The changes implement the Merchant model requirements from #39, including typed fields, camelCase mapping, validation, display_name, and package exports.
Out of Scope Changes check ✅ Passed The added base class, exports, and tests support the Merchant model work and are in scope for the linked issue.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/shade/merchant.py`:
- Line 39: In the public API field declaration for id, preserve the
schema-compatible name and add a targeted Ruff A002 suppression for this
intentional builtin shadowing. Do not rename the field or broaden the lint
suppression.
- Around line 57-58: Update the assignments in the merchant initializer to
accept only actual boolean values for active and verified, rejecting or
otherwise handling string and other non-boolean inputs instead of coercing them
with bool(). Add regression coverage confirming inputs such as "false" are not
silently converted to True.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 49965edc-1080-48fa-b836-8073ecad34dd

📥 Commits

Reviewing files that changed from the base of the PR and between 0cf3f50 and ff6c8da.

📒 Files selected for processing (4)
  • src/shade/__init__.py
  • src/shade/base.py
  • src/shade/merchant.py
  • tests/test_merchant.py

Comment thread src/shade/merchant.py Outdated
Comment thread src/shade/merchant.py Outdated
@codebestia

Copy link
Copy Markdown
Contributor

GM @giftexceed
Please address the coderabbit suggestions.

Address maintainer review feedback on ShadeProtocol#45.

- Replace bool() coercion of active/verified with a strict _require_bool
  check. bool("false") is True, so a malformed payload or a direct caller
  could silently flip a flag; non-bool values now raise InvalidRequestError
  with the offending param.
- Add a targeted "noqa: A002" on the id parameter, keeping the
  schema-compatible public field name while satisfying the builtin-shadowing
  lint.
- Add regression coverage asserting "false"/"true"/""/0/1/None are rejected
  for both flags, and that real booleans are preserved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/shade/merchant.py (1)

76-80: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Trim business_name before applying the fallback.

A whitespace-only value such as " " passes the truthiness check and is returned as display_name, skipping the first/last-name and email fallbacks. Treat trimmed-empty business names as missing.

Proposed fix
-        if self.business_name:
-            return self.business_name
+        business_name = (self.business_name or "").strip()
+        if business_name:
+            return business_name
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shade/merchant.py` around lines 76 - 80, Update the business_name branch
in the display_name logic to trim the value before checking or returning it, so
whitespace-only names are treated as missing and the existing full_name and
email fallbacks continue to run.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/shade/merchant.py`:
- Around line 76-80: Update the business_name branch in the display_name logic
to trim the value before checking or returning it, so whitespace-only names are
treated as missing and the existing full_name and email fallbacks continue to
run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a7803b8e-0981-4650-8d4d-6b5d4cf29281

📥 Commits

Reviewing files that changed from the base of the PR and between ff6c8da and df8876d.

📒 Files selected for processing (2)
  • src/shade/merchant.py
  • tests/test_merchant.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_merchant.py

Address maintainer review feedback on ShadeProtocol#45.

A whitespace-only business_name such as "   " passed the truthiness check
and was returned verbatim, skipping the full-name and email fallbacks.
Each candidate is now trimmed before it is tested and returned, so blank
values fall through to the next one.

The same latent bug applied to email, which was returned unnormalized as
the final fallback; it is now trimmed too, and display_name yields None
when every candidate is blank. The full-name branch already stripped.

Add regression coverage for whitespace-only business_name, whitespace-only
first/last names, an all-blank merchant, and trimming of the returned value.
@giftexceed

Copy link
Copy Markdown
Contributor Author

Good catch — valid, and fixed in 8a802b7.

Reproduced it first: a business_name of " " passed the truthiness check and was returned verbatim, skipping both fallbacks.

Applied the suggested trim, and extended it to email as well. email had the identical defect as the final fallback — it was returned unnormalized, so an all-whitespace email came back as " ". The full-name branch already stripped, so trimming all three keeps the contract consistent rather than leaving the same bug class in the same five-line property:

business_name = (self.business_name or "").strip()
if business_name:
    return business_name
full_name = f"{self.first_name or ''} {self.last_name or ''}".strip()
if full_name:
    return full_name
return (self.email or "").strip() or None

One resulting behavior change worth calling out: display_name now returns None when every candidate is blank, instead of a whitespace-only string.

Regression coverage added for whitespace-only business_name, whitespace-only first/last names, an all-blank merchant, and trimming of the returned value.

Validation: 163 tests passing (4 new), CI-critical flake8 (E9,F63,F7,F82) clean, flake8 @127, black --check, and ruff --select A,RUF100 clean.

@codebestia codebestia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GM @giftexceed
Good job so far.
Here are some minor change to implement.
Update your branch with the main branch, then you will notice a new models folder.
Move your merchant.py model file into the model folder and use the already implemented ShadeObject base for your Merchant model.

Update the test accordingly as well.
Thank you.

Rebuild the Merchant model on the shared pydantic ShadeObject introduced on
main (ShadeProtocol#47), replacing the standalone plain-Python model and base.

- Move src/shade/merchant.py -> src/shade/models/merchant.py and delete the
  now-redundant src/shade/base.py.
- Map camelCase JSON to snake_case fields with pydantic Field(alias=...);
  from_dict / to_dict / repr come from ShadeObject.
- Enforce validation via pydantic: StrictBool for active/verified (no
  silent coercion of strings like "false"), a Stellar public-key
  field_validator on address, and a before-validator rejecting boolean
  merchant_id (which pydantic would otherwise coerce to 1/0). All surface as
  InvalidRequestError through the base.
- Export Merchant from shade.models and the top-level package.
- Update tests: unknown keys are now preserved (extra="allow"), the
  merchant_id error param is the "merchantId" alias, and add a boolean
  merchant_id rejection case.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/shade/models/merchant.py`:
- Around line 73-79: Update the full-name fallback in the merchant display-name
property or method to strip first_name and last_name individually, discard empty
components, and join the remaining values with a single space; preserve the
existing business_name and email fallback behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e3bf7297-2211-4856-96d5-289998fc622c

📥 Commits

Reviewing files that changed from the base of the PR and between df8876d and 27304be.

📒 Files selected for processing (6)
  • src/shade/__init__.py
  • src/shade/base.py
  • src/shade/merchant.py
  • src/shade/models/__init__.py
  • src/shade/models/merchant.py
  • tests/test_merchant.py

Comment thread src/shade/models/merchant.py
@codebestia

Copy link
Copy Markdown
Contributor

@giftexceed
Please address this review

Address maintainer review feedback on ShadeProtocol#45.

The full-name fallback only stripped the combined string, so padded
components leaked internal whitespace: first_name=" Ada " and
last_name=" Lovelace " produced "Ada   Lovelace". Strip each component
and join the non-empty ones with a single space.

Add regression coverage for padded name components.

@codebestia codebestia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!
Nice implementation.
Thank you for your contribution.

@codebestia
codebestia merged commit 7652f14 into ShadeProtocol:main Jul 24, 2026
2 checks passed
@grantfox-oss grantfox-oss Bot mentioned this pull request Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Merchant model

2 participants